use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::io::Error;
use std::time::Duration;

use libp2p::gossipsub::Gossipsub;
use libp2p::gossipsub::GossipsubConfigBuilder;
use libp2p::gossipsub::GossipsubEvent;
use libp2p::gossipsub::GossipsubMessage;
use libp2p::gossipsub::MessageAuthenticity;
use libp2p::gossipsub::MessageId;
use libp2p::gossipsub::ValidationMode;
use libp2p::kad::store::MemoryStore;
use libp2p::kad::Kademlia;
use libp2p::kad::KademliaConfig;
use libp2p::kad::KademliaEvent;
use libp2p::mdns::MdnsConfig;
use libp2p::mdns::{Mdns, MdnsEvent};
use libp2p::NetworkBehaviour;

use crate::SwarmOptions;

#[derive(NetworkBehaviour)]
#[behaviour(out_event = "MyBehaviourEvent")]
pub struct MyBehaviour {
  pub mdns: Mdns,
  pub kademlia: Kademlia<MemoryStore>,
  pub gossipsub: Gossipsub,
}

impl MyBehaviour {
  pub async fn new(options: SwarmOptions) -> Result<Self, Error> {
    // 1. Kad
    // 1.1. store
    let store = MemoryStore::new(options.peer_id.to_owned());

    // 1.2. Kademlia config
    let mut kad_config = KademliaConfig::default();
    kad_config.disjoint_query_paths(true);
    kad_config.set_query_timeout(Duration::from_secs(300));

    // 1.3. create a Kademlia
    let mut kademlia = Kademlia::with_config(options.peer_id.to_owned(), store, kad_config);

    // 1.4. add boot nodes
    for (peer_id, addr) in &options.bootstrap {
      kademlia.add_address(peer_id, addr.to_owned());
    }

    // 2. Gossipsub
    // 2.1. message hash function
    let message_id_fn = |message: &GossipsubMessage| {
      let mut s = DefaultHasher::new();
      message.data.hash(&mut s);
      MessageId::from(s.finish().to_string())
    };

    // 2.2. gossipsub configuration
    let gossipsub_config = GossipsubConfigBuilder::default()
      .heartbeat_interval(Duration::from_secs(10))
      .validation_mode(ValidationMode::Strict)
      .message_id_fn(message_id_fn)
      .build()
      .expect("Valid config");

    // 2.3. create a Gossipsub
    let gossipsub = Gossipsub::new(
      MessageAuthenticity::Signed(options.keypair.clone()),
      gossipsub_config,
    )
    .expect("Correct configuration");

    // 3. mdns: peer discovery for local network
    let mdns = Mdns::new(MdnsConfig::default()).await?;

    Ok(MyBehaviour {
      mdns,
      kademlia,
      gossipsub,
    })
  }
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum MyBehaviourEvent {
  Mdns(MdnsEvent),
  Kademlia(KademliaEvent),
  Gossipsub(GossipsubEvent),
}

impl From<MdnsEvent> for MyBehaviourEvent {
  fn from(v: MdnsEvent) -> Self {
    Self::Mdns(v)
  }
}

impl From<GossipsubEvent> for MyBehaviourEvent {
  fn from(v: GossipsubEvent) -> Self {
    Self::Gossipsub(v)
  }
}

impl From<KademliaEvent> for MyBehaviourEvent {
  fn from(v: KademliaEvent) -> Self {
    Self::Kademlia(v)
  }
}
